<?php

class TH_DbSimpleQuery extends ArrayObject {

    /**
	 * Current value filter
	 * @var array
	 */
	private $filter;

	/**
	 * Filter params
	 * @var array
	 */
	private $params;

	/**
	 * Current limits
	 * @var array
	 */
	private $limit;

	/**
	 * Query string
	 * @var string
	 */
	private $_query_string;

	/**
	 * @var PDOStatement
	 */
	private $sth;

	/**
	 *
	 * TH_DbSimpleQuery state
	 * TODO: replace with int
	 * @var string
	 */
	private $state = TH_DbBase::S_READY;

	private $restricted = FALSE;

	/**
	 * @var TH_DbBase
	 */
    private $tdb;

    private $name;
    private $cls;
    private $access;
    private $binding;

    /**
     *
     * Parent item props for relation
     * @var array
     */
    private $p_props;

    /**
     * Stores size of select when data is not loaded
     * It is used and valid *only* in S_COUNT state
     * @var int
     */
    private $sizeof = 0;

    public function __construct(TH_DbBase $tdb,
                                $target_name,
                                $access=TH_DbBase::A_FULL,
                                Array $binding=NULL,
                                Array &$p_props=NULL) {

        $this->tdb = $tdb;
        $this->name = $target_name;
        $this->cls = 'TH_DbTbl_'. ucfirst($target_name);
        $this->access   = $access;

        $szb = sizeof($binding);
        if ($binding != NULL && ($szb < 2 || $szb > 3)) { // fool-proof check
            throw new Exception("Wrong binding on {$this->name} ({$this->cls})");
        }

        $this->binding  = $binding;
        $this->p_props = &$p_props;

        $this->clearAll();

        if ($this->binding) { // if binding given it means that query is relational, so put it in restriction mode
            $this->restricted = TRUE;
        }
    }

    public function clearAll() {
        $this->clearFilter()
             ->clearLimit()
             ->clearParams();
        return $this;
    }

    /**
	 * @return TH_DbSimpleQuery
	 */
	public function setLimit($from=NULL, $count=NULL) {
	    $this->limit = array("from" => NULL, "count" => $count);

	    $this->_clear();

	    return $this;
	}

	/**
	 * @return TH_DbSimpleQuery
	 */
	public function clearLimit() {
	    $this->setLimit(NULL, NULL);
	    return $this;
	}

	/**
	 * @return TH_DbSimpleQuery
	 */
	protected function applyLimit(&$query_string) {
	    if ($this->limit['from']) {
	        $query_string .= ' LIMIT '. $this->limit['from']
	                      . ', ' . $this->limit['count'] ? $this->limit['count'] : '18446744073709551615';
	    } elseif ($this->limit['count']) {
	        $query_string .= ' LIMIT '. $this->limit['count'];
	    }
	    return $this;
	}

	/**
	 * @return TH_DbSimpleQuery
	 */
	public function setFilter(Array $filter) {
	    if ($this->restricted) {
	        throw new Exception("Setting filter is disabled in rectricted mode on {$this->name} ({$this->cls})");
	    }
	    $this->filter = $filter;

	    $this->_clear();
	    return $this;
	}

	/**
	 * @return TH_DbSimpleQuery
	 */
	public function clearFilter() {
	    $this->setFilter(array());
	    return $this;
	}

	/**
	 * @return TH_DbSimpleQuery
	 */
	public function setParams(Array &$params) {
	    if ($this->restricted) {
	        throw new Exception("Setting params is disabled in rectricted mode on {$this->name} ({$this->cls})");
	    }

	    $this->params &= $params;
	    return $this;
	}

	/**
	 * @return TH_DbSimpleQuery
	 */
	public function clearParams() {
	    $this->params = array();
	    return $this;
	}

	/**
	 * TODO: add compare function-wrappers, something like this:
	 * 		$xxx->addFilter(array("id" => TDB::and(TDB::gt(1), TDB::lt(123)), ... ));
	 * @return TH_DbSimpleQuery
	 */
	protected function applyFilter(&$query_string) {
//	    $query_string .= "WHERE 1";
	    foreach ($this->filter as $name => $value) {
            $query_string .= " AND {$name} = {$value}";    // NOTE: we may ommit colon and use filter to bind tables
        }

	    return $this;
	}

	protected function buildSelect($count=FALSE) {
	    $this->_query_string = $count
	                               ? "SELECT COUNT(*) as `count` FROM"
	                               : "SELECT {$this->name}.* FROM";

	    if ($this->binding) {
	        $tables = array();
	        $where  = array();

	        $sz = sizeof($this->binding);

	        for ($i=0; $i<$sz; $i++) {
	            $rt = $this->binding[$i];    // one of relation table

	            if (TH_DbBase::D_STORAGE == $rt[0]) {
	                list($type, $name, $key) = $rt;
	                array_push($tables, "th__{$name}_s AS {$name}");
	                array_push($where, array($name, $key));
	            } elseif (TH_DbBase::D_BROKER == $rt[0]) {
	                list($type, $name) = $rt;
	                array_push($tables, "th__{$name}_b AS {$name}");
	                $where['broker'] = $name;
	            } else {
	                throw new Exception("Bad binding {$rt[1]} on {$this->name}"); // $rt[1] is table name
	            }

	        }

	        $this->_query_string .= ' ' . implode(', ', $tables) . ' WHERE 1';

	        // TODO: migrate on $this->filter
	        // TODO: use iteration here on $this->binding
	        if (array_key_exists('broker', $where)) {
	            // table1.key = broker.table1_key
                $this->_query_string .= " AND {$where[0][0]}.{$where[0][1]} = {$where['broker']}.{$where[0][0]}__{$where[0][1]}";
	            // table2.key = broker.table2_key
                $this->_query_string .= " AND {$where[1][0]}.{$where[1][1]} = {$where['broker']}.{$where[1][0]}__{$where[1][1]}";
	        } else {
	            // table1.key = table2.key
                $this->_query_string .= " AND {$where[0][0]}.{$where[0][1]} = {$where[1][0]}.{$where[1][1]}";
	        }
	        // XXX: not clear, but next setFilter resets previous filters.
	        // Don't forget to pass necessary param!
	        //     >> bohdan.padalko 24/08/11 01:18:    in restricted mode it will be set automatically
	        // base id sets here: table1.key = :table1__key
            $this->_query_string .= " AND {$where[0][0]}.{$where[0][1]} = :{$where[0][0]}__{$where[0][1]}";
	    } else {
	        // TODO: works with filter, param and WHERE 1
            $this->_query_string .= " th__{$this->name}_s AS {$this->name} WHERE 1"; // we needs WHERE 1 to
	    }
        return $this;
	}

	/**
	 * Build query string
	 * @return TH_DbSimpleQuery
	 */
	protected function _build($count=FALSE) {
	    $this->_clear();    // if previous build was for COUNT, then state is not S_READY and S_LOADED
	    $this->buildSelect($count)
             ->applyFilter($this->_query_string)
             ->applyLimit($this->_query_string);

        $this->sth = $this->tdb->prepare($this->_query_string);

        return $this;
	}

	/**
	 * Clear query string
	 * @return TH_DbSimpleQuery
	 */
	protected function _clear() {
	    if (TH_DbBase::S_READY != $this->state ) {
    	    $this->sth = NULL;
    	    $this->_query_string = NULL;
    	    // NOTE: what should be done with modified objects?
    	    parent::exchangeArray(array());    // remove all loaded data
    	    $this->state = TH_DbBase::S_READY;
	    }
	    return $this;
	}

	protected function _query($count=FALSE) {
        $this->_build($count);    // TH_DbBase::S_READY check is in $this->offsetGet method

	    xdbg()->info("Query string", $this->_query_string);

	    if ($this->restricted) {    // in restricted mode we always access key value, so it must be readable at least?

	        list($type, $name, $key) = $this->binding[0];
            $k = array(":{$name}__{$key}" => $this->p_props[$key]['s.value']);
            $this->sth->execute($k);
	    } else {
            $this->sth->execute($this->params);
	    }

        if (0 != $this->sth->errorCode()) {    // die on error
            $e = $this->sth->errorInfo();
            throw new Exception("Error : db:" .$e[1] . ":" . $e[2]);
        }

        $res_data = $this->sth->fetchAll(PDO::FETCH_ASSOC);

        if ($count) {
            $this->sizeof = (int) $res_data[0]['count'];
            $this->state = TH_DbBase::S_COUNT;
        } else {
            parent::exchangeArray($res_data);    // load result data into itself
            $this->state = TH_DbBase::S_LOADED;
        }

        return $this;
	}

	public function count() {
	    if (TH_DbBase::S_LOADED == $this->state) {
	        return parent::count();
	    }

	    if (TH_DbBase::S_READY == $this->state) {
	        $this->_query(TRUE);    // XXX: sizeof(TH_DbQuery object ) now do not run full query when select not performed, just SELECT COUNT(lalal) FROM ...

	    }
	    return $this->sizeof;
	}

	public function offsetGet($index) {
//	    xdbg()->msg('test state in getter', $index, $this->state);
	    if (TH_DbBase::S_LOADED != $this->state
	        && TH_DbBase::S_MODIFIED != $this->state) {
	        $this->_query();
	    }

	    if (!parent::offsetExists($index)) {
	        throw new Exception("Wrong offset $index on {$this->name} ({$this->cls})");
	    }

	    if (!is_object(parent::offsetGet($index))) {
            parent::offsetSet($index, new $this->cls($this->tdb,
                                                          $this->name,
                                                          parent::offsetGet($index),
                                                          TH_DbItem::S_OK
                                						 )
                             );

//            parent::offsetGet($index)->bind('on_change___object', array(&$this)); // bind SLOT to handle child state
	    }

	    return parent::offsetGet($index);
	}

	function offsetSet($index, $newval) {
	    if (!parent::offsetExists($index)) {
	        throw new Exception("Wrong offset $index on {$this->name} ({$this->cls})");
	    }

	    // TODO: implement
	    throw new Exception("Not implemented for $index");

	    // TODO: run base copy

	    if (is_array($newval)) {
	        // fill from array
	    } elseif (is_a($newval, $this->cls)) {
	        // fill from one of TH_DbItem's child object
	    } else {
	        throw new Exception("Invalid value type to set at {$index} on {$this->name} ({$this->cls})");
	    }

//	    parent::offsetSet($index, new $this->cls($this->tdb,
//                                                          $this->name,
//                                                          parent::offsetGet($index),
//                                                          TH_DbBase::I_OLD
//                                						 )
//                           );

	    $this->state = TH_DbBase::S_MODIFIED;
        return $this;
	}

	public function offsetUnset($index) {
	    // TODO: implement
	    throw new Exception("Not implemented for $index");
	}

	public function _TODO_exchangeArray(Array $input) {
	    // TODO: implement
	    throw new Exception("Not implemented: exchangeArray");
        foreach ($input as $k => $v) {
            if (is_array($v)) {
                // construct new TH_DbItem
                $input[$k] = new $this->cls($this->tdb,
                                            $this->name,
                                            $v,
                                            TH_DbItem::S_NEW
                                           );
            } elseif (is_a($v, $this->cls)) {
                // everything OK
            } else {
                throw new Exception("Invalid item type in given array on {$this->name} ({$this->cls})");
            }
        }

	}

	public function save() {

	    $iterator = $this->getIterator();

	    while($iterator->valid()) {
	        xdbg()->msg('save', $iterator->key());
	        $iterator->current()->save();
            $iterator->next();
        }
	}


}

?>